package org.web.demo.servlet;

import lombok.extern.slf4j.Slf4j;

import javax.servlet.*;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.*;

/**
 * 什么是 Servlet: Server Applet的简称，称为"服务端小程序"，是 JavaEE 平台下的技术标准，基于 Java 语言编写的服务端程序。
 * Web容器或应用服务器实现了 Servlet 标准所以 Servlet 需运行在 Web 容器或应用服务器中。Servlet主要功能在于能在服务器中执行并生成数据。
 * <p>
 * Servlet技术特点：Servlet使用单进程多线程方式运行。
 * <p>
 * 静态资源和动态资源区分：
 * 静态资源: 每次访问都不需要运算,直接就可以返回的资源, 如HTML  CSS  JS  多媒体文件等等,每次访问获得的资源都是一样的
 * 动态资源:每次访问都需要运算代码生成的资源,如Servlet JSP ,每次访问获得的结果可能都是不一样的
 * Servlet作为一种动态资源技术,是我们后续学习框架的基础
 * <p>
 * Servlet在程序中的地位：
 * Servlet是通过JAVA语言编写的一种可以接受Http请求并作出相应处理的一种动态资源技术。
 * Servlet是前后端衔接的一种技术,不是所有的JAVA类都可以接收请求和作出相应,Servlet可以。
 * 在 MVC 模式中，Servlet 作为 Controller 层(控制层)主要技术,用于和浏览器完成数据交互,控制交互逻辑
 * <p>
 * GenericServlet抽象类：
 * GenericServlet是实现了Servlet接口的抽象类。在GenericServlet中进一步的定义了Servlet接口的具体实现，其设计的目的是为了和应用层协议解耦。
 * （该类是能够让处理Http协议与处理其他协议的Servlet均能继承使用的抽象类）
 * 在GenericServlet中包含一个service抽象方法，我们也可以通过继承GenericServlet并实现service方法来实现请求的处理，
 * 但是其定义的参数是 ServletRequest 和 ServletResponse ，如果是处理Http协议，我们就需要转为 HttpServletRequest 和 HttpServletResponse。
 * <p>
 * HttpServlet：
 * 继承了GenericServlet的实现类。是针对于处理Http协议的请求和响应所定制的Servlet类。
 * 在 HttpServlet的service() 方法中已经把 ServletRequest 和 ServletResponse 转为 HttpServletRequest 和 HttpServletResponse。
 * 因此，在实际开发中，当涉及 Http 处理时，我们直接继承 HttpServlet 使用即可。
 * <p>
 * Servlet的生命周期:
 * Servlet的生命周期是由容器管理的，分别经历四个阶段：实例化->init()->service()->destroy()
 *
 * @author yexi
 */
@Slf4j
public class DemoServlet extends HttpServlet {

    public DemoServlet() {
        log.info("DemoServlet对象被实例化。" +
                "仅客户浏览器第一次请求时会调用一次(由于配置了load-on-startup所以会在容器开始时便创建)。");
    }

    /**
     * 由 servlet 容器调用。servlet 容器在实例化 servlet 后调用一次init方法。
     * init方法必须成功完成，servlet 才能接收任何请求。
     * 如果init方法抛出ServletException或者在 Web 服务器定义的时间段内不返回，则 servlet 容器无法将 servlet 投入服务。
     */
    @Override
    public void init() throws ServletException {
        log.info("DemoServlet初始化。Servlet对象实例化之后调用。");
    }


    /**
     * 由 servlet 容器调用。只有在 servlet 的service方法中的所有线程都退出或经过超时时间之后，才会调用此方法。
     * servlet容器调用该方法后，不会再在该servlet上调用service方法。
     * 此方法使 servlet 有机会清理任何被占用的资源（例如，内存、文件句柄、线程），并确保任何持久状态(persistent state)与 servlet 在内存中的当前状态同步。
     */
    @Override
    public void destroy() {
        log.info("destroy(): Servlet销毁。在销毁Servlet对象之前调用该方法，释放资源，仅调用一次。");
    }

    /**
     * ServletContext: Servlet上下文。
     * 服务器会为每一个Web应用创建一个ServletContext对象。这个对象全局唯一，而且Web应用中的所有Servlet都共享这个对象。
     * 所以又叫全局应用程序共享对象。
     * <p>
     * 当容器启动时该对象便会创建并一直缓存，直到容器关闭后该对象生命周期结束。
     * ServletContext对象的生命周期非常长，所以在使用全局容器时不建议存放业务数据。
     */
    @Override
    public ServletContext getServletContext() {
        ServletContext servletContext = super.getServletContext();
        log.info("获取ServletContext配置(全局配置)");
        log.info("当前项目的部署名: " + servletContext.getContextPath());
        log.info("将一个相对路径转化为项目的绝对路径: " + servletContext.getRealPath("test"));
        log.info("Web容器附加信息: " + servletContext.getServerInfo());
        log.info("Web容器所支持Servlet的主版本号: " + servletContext.getMajorVersion());
        log.info("Web容器所支持Servlet的副版本号: " + servletContext.getMinorVersion());
        log.info("ServletContext名称: " + servletContext.getServletContextName());

        // 获取web.xml中配置的全局的初始信息(从 <context-param> 中获取)
        // getInitParameterNames: 获取所有的 <param-name> 标签中的值
        Enumeration<String> initParameterNames = servletContext.getInitParameterNames();
        while (initParameterNames.hasMoreElements()) {
            String initParameterName = initParameterNames.nextElement();
            //getInitParameter: 获取指定 <param-name> 对应的 <param-value> 的值
            log.info("初始名称: " + initParameterName + " 初始值:" + servletContext.getInitParameter(initParameterName));
        }


        // 获取当前Servlet的配置信息(从 <servlet> 中获取)
        ServletConfig servletConfig = this.getServletConfig();
        // this.getInitParameterNames()等效于servletConfig.getInitParameterNames(),但它们返回的Enumeration对象是不一致的注意了。
        // (因为调用了Collections.enumeration()返回了新对象)
        // getInitParameterNames: 获取所有的 <init-param> 中的 <param-name> 标签中的值
        Enumeration<String> initParameterNamesServletConfig = this.getInitParameterNames();
        while (initParameterNamesServletConfig.hasMoreElements()) {
            String initParameterName = initParameterNamesServletConfig.nextElement();
            //getInitParameter: 获取指定 <param-name> 对应的 <param-value> 的值
            log.info("初始名称: " + initParameterName + " 初始值:" + servletConfig.getInitParameter(initParameterName));
        }

        log.info("--------------------");
        //获取ServletContext对象中所有数据域对象
        Enumeration<String> attributeNames = servletContext.getAttributeNames();
        while (attributeNames.hasMoreElements()) {
            String attributeName = attributeNames.nextElement();
            log.info("初始属性: " + attributeName + " 初始值:" + servletContext.getAttribute(attributeName));
        }
        //向ServletContext对象中增加数据 域对象
        servletContext.setAttribute("list", Arrays.asList("张三", "李四", "王五"));
        servletContext.setAttribute("gender", "man");
        log.info(servletContext.getAttribute("list").toString());
        log.info((String) servletContext.getAttribute("gender"));
        servletContext.removeAttribute("gender");

        return servletContext;
    }

    /**
     * ServletConfig接口：
     * 当Tomcat初始化一个Servlet时，会将该Servlet的配置信息(对应web.xml文件中的<servlet>节点或者Servlet类上的注解)封装到一个ServletConfig对象中。
     * 然后通过调用init(ServletConfig config)方法，将ServletConfig对称传递给Servlet。
     * <p>
     * 获取当前Servlet的配置信息。
     */
    @Override
    public ServletConfig getServletConfig() {
        log.info("--------------------");
        log.info("获取当前Servlet配置信息");
        ServletConfig servletConfig = super.getServletConfig();
        return servletConfig;
    }

    /**
     * 返回有关servlet的信息，如作者、版本和版权。
     * 此方法返回的字符串应该是纯文本，而不是任何类型的标记语言（如HTML、XML等）。
     * TIPS: 此方法没有默认实现，需要自己实现。以上描述为接口定义规范。
     */
    @Override
    public String getServletInfo() {
        return super.getServletInfo();
    }

    /**
     * 获取当前ServletConfig的初始化参数信息
     */
    @Override
    public Enumeration<String> getInitParameterNames() {
        log.info("--------------------");
        log.info("获取初始化参数名称");
        Enumeration<String> initParameterNames = super.getInitParameterNames();
        return initParameterNames;
    }

    /**
     * service：Servlet的服务方法。可以接收浏览器的请求，并作出运算和响应。
     *
     * @param req  HttpServletRequest对象代表客户端浏览器的Http请求。
     *             当客户端浏览器通过HTTP协议访问服务器时，HTTP请求中的所有信息都会被Tomcat所解析并封装在这个对象中，
     *             通过这个对象提供的方法，可以获得客户端请求的所有信息。
     * @param resp HttpServletResponse对象代表服务器的响应。
     *             这个对象中封装了响应客户端浏览器的流对象，以及向客户端浏览器响应的响应头、响应数据、响应状态码等信息。
     */
    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        log.info("service: Servlet执行服务。每次处理浏览器的请求都会调用该方法");

        //获取Servlet上下文对象
        ServletContext servletContext1 = req.getServletContext();
        ServletContext servletContext2 = this.getServletContext();
        log.info("是否同一个servlet上下文对象(是否全局共享): " + (servletContext1 == servletContext2));


        log.info("----------Request----------");
        log.info("请求方式:" + req.getMethod());
        if ("POST".equals(req.getMethod())) {
            //主动调用doPost定制化处理post请求
            this.doPost(req, resp);
        } else if ("GET".equals(req.getMethod())) {
            //主动调用doGet定制化处理get请求
            this.doGet(req, resp);
        }

        String username = null;
        String pwd = null;
        log.info("获取请求中所有数据的参数名称(Get时在URL上，POST时在请求体内): ");
        Enumeration<String> parameterNames = req.getParameterNames();

        while (parameterNames.hasMoreElements()) {
            String parameterName = parameterNames.nextElement();
            if ("用户名".equals(parameterName)) {
                username = req.getParameter(parameterName);
            }
            if ("密码".equals(parameterName)) {
                pwd = req.getParameter(parameterName);
            }
            //getParameter:根据请求key获取对应的请求参数值
            log.info("参数名称: " + parameterName + " 参数对应值: " + req.getParameter(parameterName));
        }
        // 判断数据
        String message;
        if ("管理员".equals(username) && "密码".equals(pwd)) {
            message = "登录成功";
        } else {
            message = "登录失败";
        }

        //getParameter方法只能获取一个值，如果一个同名参数对应多个值，则需要使用getParameterValues，一般用于获取复选框选中的所有值
        String[] checkboxes = req.getParameterValues("复选框");
        log.info("复选框中的所有值: " + Arrays.toString(checkboxes));

        //获取请求中所有的数据并存放到一个Map结构中，key为请求参数，value为对应的值
        // getParameterNames()相当于获取了该Map所有的key
        // getParameterValues(key)相当于获取了该Map对应的value
        Map<String, String[]> parameterMap = req.getParameterMap();
        for (Map.Entry<String, String[]> entry : parameterMap.entrySet()) {
            log.info("参数名称: " + entry.getKey() + " 对应所有值: " + Arrays.toString(entry.getValue()));
        }

        //getAttributeNames(): 获取本次 request 作用域中设置的所有名称
        //getAttribute(name): 获取名称对应值
        // 该方法由于只作用在一次 request 中，所以写在service()方法里没有意义，一般使用于jsp当中。
        Enumeration<String> attributeNames = req.getAttributeNames();
        while (attributeNames.hasMoreElements()) {
            String attributeName = attributeNames.nextElement();
            log.info(("request作用域属性名称: " + attributeName + " 对应值: " + req.getAttribute(attributeName)));
        }


        log.info("客户端浏览器发出请求时的完整URL: " + req.getRequestURL());
        log.info("请求行中指定资源部分: " + req.getRequestURI());
        System.out.println("发出请求的客户机的IP地址: " + req.getRemoteAddr());
        System.out.println("WEB服务器的IP地址: " + req.getLocalAddr());
        System.out.println("WEB服务器处理Http协议的连接器所监听的端口: " + req.getLocalPort());
        System.out.println("主机名: " + req.getLocalName());
        System.out.println("客户端端口: " + req.getRemotePort());
        System.out.println("当前项目部署名: " + req.getContextPath());
        System.out.println("协议:" + req.getProtocol());
        System.out.println("协议名:" + req.getScheme());

        Enumeration<String> headerNames = req.getHeaderNames();
        System.out.println("输出所有的请求头信息: ");
        while (headerNames.hasMoreElements()) {
            String headerName = headerNames.nextElement();
            //getHeader:根据请求头名称获取对应的值
            System.out.println("请求头名称: " + headerName + " 请求头对应值: " + req.getHeader(headerName));
        }


        System.out.println("----------Response----------");
        //设置响应码。请求成功默认返回200。
        resp.setStatus(302);
        resp.setHeader("custom-responseHeader", "自定义值");
        // 设置服务端为浏览器产生响应的响应编码，服务端会根据此编码将响应内容的字符转换为字节。
        resp.setCharacterEncoding("UTF-8");
        // text/html: 设置响应类型为文本型，内容含有html字符串。是默认的响应类型。
        // charset=utf-8:设置服务端为浏览器产生响应的响应编码，服务端会根据此编码将响应内容的字符转换为字节。
        // 同时设置 Content-Type 响应头，告知浏览器以utf-8编码解析数据
        resp.setContentType("text/html;charset=utf-8");
        // 从响应对象中取出打印流，该打印流指向了浏览器，可以对浏览器作出响应。
        PrintWriter writer = resp.getWriter();
        writer.write("<h1>" + message + "</h1>");
    }

    /**
     * 在我们自定义的Servlet中，如果想区分请求方式，不同的请求方式使用不同的代码处理，那么我们可以重写 doGet  doPost 等方法。
     * 如果没有必要区分，直接重写service即可。
     * 注意的是，隐式调用它们的本质上是未重写时的 service 方法，所以如果重写了service方法就没有隐式调用了。
     * 一般而言，重写 do 这类方法和重写service是二选一的。当然如果都想用也可以，那么就需要在重写后的service自行调用才可以了。
     *
     * @see HttpServlet#service(HttpServletRequest, HttpServletResponse)
     */
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        // 处理post请求编码(get请求编码Tomcat9后已自行解决,Tomcat9之前需要手动进行编码解码,或者设置server.xml中的URIEncoding="utf-8")
        // (请求的数据包基于字节在网络上传输，Tomcat接收到请求的数据包后会将数据包中的字节转换为字符。
        // 由于tomcat使用ISO-8859-1编码来完成字节与字符的转换，所以中文会乱码，
        // 可以通过该方法重新设置让tomcat对提交的数据根据指定的编码方式重新做编码处理。)
        req.setCharacterEncoding("UTF-8");
    }

    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

    }
}
